---
sidebar_position: 4
---

# Custom transport

The main job of a transport is to create ADB sockets. `AdbDaemonTransport` creates ADB sockets using low-level ADB packets, `AdbServerTransport` creates ADB sockets by sending commands to an ADB server. ADB sockets are what the ADB commands work on.

While there might not be any other transports that can directly create ADB sockets, it's still useful to create custom transports for other purposes. For example, a mock transport that returns fixed data can be used in tests, or a WebSocket transport can wrap a built-in transport for accessing devices over the Internet.

## ADB Socket

Each ADB socket is basically a pair of `ReadableStream<Uint8Array>` and `WritableStream<Consumable<Uint8Array>>`.

:::note

See [Web Streams Basics](./web-stream.mdx) for a quick introduction to `ReadableStream`, `WriteableStream`, and other types from [Web Streams API](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API).

:::

```ts
import type { Consumable } from "@yume-chan/stream-extra";

interface AdbSocket {
  readonly service: string;

  readable: ReadableStream<Uint8Array>;
  writable: WritableStream<Consumable<Uint8Array>>;

  close(): void | Promise<void>;
}
```

### `service`

`service` is sent to the device when creating the socket. It's like the URL in an HTTP request, and the device will decide how to handle the request based on the service.

### `readable`

`readable` contains the data sent from the device. It's a `ReadableStream<Uint8Array>`, which means it's a stream of `Uint8Array`s. ADB socket is a stream protocol, so chunks can be split or combined arbitrarily.

The consumer can call `readable.cancel()` to signal the lost of interest in the data. The producer must throw away all incoming data after `readable.cancel()` is called. Calling `readable.cancel()` doesn't close the socket, and `writable` can still be used to send data to the device.

### `writable`

`writable` is used to send data to the device. The `Consumable` pattern is used to allow the producer to know when the data is consumed, and the buffer can be reused. The transport must only mark the `Consumable` as consumed when the data is actually sent to the device, or has been copied to another buffer for sending.

Similar to `readable.cancel()`, `writable.close()` does not close the socket. The implementer can ignore the call. The `WritableStream` itself will prevent further writes.

## ADB Transport

```ts
export interface AdbTransport {
  readonly serial: string;

  readonly maxPayloadSize: number;

  readonly banner: AdbBanner;

  readonly disconnected: Promise<void>;

  connect(service: string): AdbSocket | Promise<AdbSocket>;

  addReverseTunnel(
    handler: AdbIncomingSocketHandler,
    address?: string,
  ): string | Promise<string>;

  removeReverseTunnel(address: string): void | Promise<void>;

  clearReverseTunnels(): void | Promise<void>;

  close(): void | Promise<void>;
}
```

As mentioned before, the main function of `AdbTransport` is the `connect` method, which creates an ADB socket for a service (request).

Other fields:

- `serial`: The serial number of the device. It's not used by Tango, but it helps you to identify the device.
- `maxPayloadSize`: The maximum payload size of the device. `AdbSync#write` will split data into multiple packets if it's larger than this value. If you don't know this value, use 4KB.
- `banner`: ADB banner returned from the device. It contains the list of ADB features the device supports, which is important for many command to choose the correct behavior.
- `disconnected`: A promise that resolves when the transport is disconnected.
- `addReverseTunnel`, `removeReverseTunnel`, `clearReverseTunnels`: These methods are used to manage reverse tunnels. When a reverse tunnel is created, Tango sends the correct command to ADB daemon via `connect` method, but also calls `addReverseTunnel` method. The transport should register the reverse tunnel, so when ADB daemon creates a socket to the tunnel address, the `handler` method will be invoked. If the transport doesn't support reverse tunnel, it should throw an error from `addReverseTunnel` method. See [Reverse Tunnel](../api/reverse.mdx) for more details.
- `close`: Close the transport. It should close all sockets created by `connect` method, and resolve `disconnected` promise.

## Example

This creates a mock transport that responds to `getprop` shell command:

```ts transpile
import {
  AdbBanner,
  AdbIncomingSocketHandler,
  AdbSocket,
  AdbTransport,
} from "@yume-chan/adb";
import { PromiseResolver } from "@yume-chan/async";
import {
  Consumable,
  ReadableStream,
  WritableStream,
} from "@yume-chan/stream-extra";
import { ValueOrPromise, encodeUtf8 } from "@yume-chan/struct";

class MockSocket implements AdbSocket {
  service: string;
  readable: ReadableStream<Uint8Array>;
  writable: WritableStream<Consumable<Uint8Array>>;

  closed = false;

  constructor(service: string, response: Uint8Array) {
    this.service = service;

    this.readable = new ReadableStream({
      start(controller) {
        controller.enqueue(response);
        controller.close();
      },
    });

    this.writable = new WritableStream({
      write(chunk) {
        if (closed) {
          throw new Error("Socket closed");
        } else {
          console.log(chunk);
        }
      },
    });
  }

  close(): ValueOrPromise<void> {
    this._closed = true;
  }
}

class MockTransport implements AdbTransport {
  get serial(): string {
    return "12345678";
  }

  get maxPayloadSize(): number {
    return 4 * 1024;
  }

  get banner(): AdbBanner {
    return new AdbBanner("mock", "mock", "mock", []);
  }

  #disconnected = new PromiseResolver<void>();
  get disconnected(): Promise<void> {
    return this.#disconnected.promise;
  }

  connect(service: string): AdbSocket {
    if (service.startsWith("shell:getprop")) {
      const key = service.split(" ")[1];
      switch (key) {
        case "ro.product.model":
          return new MockSocket(service, encodeUtf8("Pixel 3a"));
      }
    }

    throw new Error(`Unknown service: ${service}`);
  }

  addReverseTunnel(
    handler: AdbIncomingSocketHandler,
    address?: string | undefined,
  ): ValueOrPromise<string> {
    throw new Error("Method not implemented.");
  }

  removeReverseTunnel(address: string): ValueOrPromise<void> {
    throw new Error("Method not implemented.");
  }

  clearReverseTunnels(): ValueOrPromise<void> {
    throw new Error("Method not implemented.");
  }

  close(): ValueOrPromise<void> {
    this.#disconnected.resolve();
  }
}
```
